package com.immediatus.graphics.util;

import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLException;
import android.os.Build;
import com.immediatus.options.RenderOptions;
import com.immediatus.util.FastFloatBuffer;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import static com.immediatus.graphics.texture.Texture.PixelFormat;

public class OpenGLWrapper{
    public static final int BYTES_PER_FLOAT = 4;
    private static final boolean IS_LITTLE_ENDIAN = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN);
    private static final int[] HARDWARETEXTUREID_CONTAINER = new int[1];
    private static final int[] HARDWAREBUFFERID_CONTAINER = new int[1];
    public static boolean EXTENSIONS_VERTEXBUFFEROBJECTS = false;
    public static boolean EXTENSIONS_DRAWTEXTURE = false;
    public static boolean EXTENSIONS_TEXTURE_NON_POWER_OF_TWO = false;

    private static int _currentHardwareBufferID = -1;
    private static int _currentHardwareTextureID = -1;
    private static int _currentMatrix = -1;

    private static int _currentSourceBlendMode = -1;
    private static int _currentDestinationBlendMode = -1;

    private static FastFloatBuffer _currentVertexFloatBuffer = null;
    private static FastFloatBuffer _currentTextureFloatBuffer = null;

    private static boolean _enableDither = true;
    private static boolean _enableLightning = true;
    private static boolean _enableDepthTest = true;
    private static boolean _enableMultisample = true;

    private static boolean _enableScissorTest = false;
    private static boolean _enableBlend = false;
    private static boolean _enableCulling = false;
    private static boolean _enableTextures = false;
    private static boolean _enableTexCoordArray = false;
    private static boolean _enableVertexArray = false;
    private static boolean _depthMaskEnabled = false;

    private static float _lineWidth = 1;

    private static float _red = -1;
    private static float _green = -1;
    private static float _blue = -1;
    private static float _alpha = -1;

    public static void reset(final GL10 gl_){
        OpenGLWrapper._currentHardwareBufferID = -1;
        OpenGLWrapper._currentHardwareTextureID = -1;
        OpenGLWrapper._currentMatrix = -1;

        OpenGLWrapper._currentSourceBlendMode = -1;
        OpenGLWrapper._currentDestinationBlendMode = -1;

        OpenGLWrapper._currentVertexFloatBuffer = null;
        OpenGLWrapper._currentTextureFloatBuffer = null;

        OpenGLWrapper.enableDither(gl_);
        OpenGLWrapper.enableLightning(gl_);
        OpenGLWrapper.enableDepthTest(gl_);
        OpenGLWrapper.enableMultisample(gl_);

        OpenGLWrapper.disableBlend(gl_);
        OpenGLWrapper.disableCulling(gl_);
        OpenGLWrapper.disableTextures(gl_);
        OpenGLWrapper.disableTexCoordArray(gl_);
        OpenGLWrapper.disableVertexArray(gl_);

        OpenGLWrapper._lineWidth = 1;

        OpenGLWrapper._red = -1;
        OpenGLWrapper._green = -1;
        OpenGLWrapper._blue = -1;
        OpenGLWrapper._alpha = -1;

        OpenGLWrapper.EXTENSIONS_VERTEXBUFFEROBJECTS = false;
        OpenGLWrapper.EXTENSIONS_DRAWTEXTURE = false;
        OpenGLWrapper.EXTENSIONS_TEXTURE_NON_POWER_OF_TWO = false;
    }

    public static void enableExtensions(final GL10 gl_, final RenderOptions renderOptions_){
        final String version = gl_.glGetString(GL10.GL_VERSION);
        final String renderer = gl_.glGetString(GL10.GL_RENDERER);
        final String extensions = gl_.glGetString(GL10.GL_EXTENSIONS);

        final boolean isOpenGL10 = version.contains("1.0");
        final boolean isOpenGL2X = version.contains("2.");
        final boolean isSoftwareRenderer = renderer.contains("PixelFlinger");
        final boolean isVBOCapable = extensions.contains("_vertex_buffer_object");
        final boolean isDrawTextureCapable = extensions.contains("draw_texture");
        final boolean isTextureNonPowerOfTwoCapable = extensions.contains("texture_npot");

        OpenGLWrapper.EXTENSIONS_VERTEXBUFFEROBJECTS =
                !renderOptions_.isDisableExtensionVertexBufferObjects() && !isSoftwareRenderer &&
                        (isVBOCapable || !isOpenGL10);
        OpenGLWrapper.EXTENSIONS_DRAWTEXTURE =
                !renderOptions_.isDisableExtensionVertexBufferObjects() && (isDrawTextureCapable || !isOpenGL10);
        OpenGLWrapper.EXTENSIONS_TEXTURE_NON_POWER_OF_TWO = isTextureNonPowerOfTwoCapable || isOpenGL2X;

        OpenGLWrapper.hackBrokenDevices();
    }

    private static void hackBrokenDevices(){
        if (Build.PRODUCT.contains("morrison")){
            OpenGLWrapper.EXTENSIONS_VERTEXBUFFEROBJECTS = false;
        }
    }

    public static void setColor(final GL10 gl_, final float red_, final float green_, final float blue_, final float alpha_){
        if (alpha_ != OpenGLWrapper._alpha || red_ != OpenGLWrapper._red || green_ != OpenGLWrapper._green ||
                blue_ != OpenGLWrapper._blue){
            OpenGLWrapper._alpha = alpha_;
            OpenGLWrapper._red = red_;
            OpenGLWrapper._green = green_;
            OpenGLWrapper._blue = blue_;
            gl_.glColor4f(red_, green_, blue_, alpha_);
        }
    }

    public static void enableVertexArray(final GL10 gl_){
        if (!OpenGLWrapper._enableVertexArray){
            OpenGLWrapper._enableVertexArray = true;
            gl_.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        }
    }

    public static void disableVertexArray(final GL10 gl_){
        if (OpenGLWrapper._enableVertexArray){
            OpenGLWrapper._enableVertexArray = false;
            gl_.glDisableClientState(GL10.GL_VERTEX_ARRAY);
        }
    }

    public static void enableTexCoordArray(final GL10 gl_){
        if (!OpenGLWrapper._enableTexCoordArray){
            OpenGLWrapper._enableTexCoordArray = true;
            gl_.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        }
    }

    public static void disableTexCoordArray(final GL10 gl_){
        if (OpenGLWrapper._enableTexCoordArray){
            OpenGLWrapper._enableTexCoordArray = false;
            gl_.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        }
    }

    public static void enableScissorTest(final GL10 gl_){
        if (!OpenGLWrapper._enableScissorTest){
            OpenGLWrapper._enableScissorTest = true;
            gl_.glEnable(GL10.GL_SCISSOR_TEST);
        }
    }

    public static void disableScissorTest(final GL10 gl_){
        if (OpenGLWrapper._enableScissorTest){
            OpenGLWrapper._enableScissorTest = false;
            gl_.glDisable(GL10.GL_SCISSOR_TEST);
        }
    }

    public static void enableBlend(final GL10 gl_){
        if (!OpenGLWrapper._enableBlend){
            OpenGLWrapper._enableBlend = true;
            gl_.glEnable(GL10.GL_BLEND);
        }
    }

    public static void disableBlend(final GL10 gl_){
        if (OpenGLWrapper._enableBlend){
            OpenGLWrapper._enableBlend = false;
            gl_.glDisable(GL10.GL_BLEND);
        }
    }

    public static void enableCulling(final GL10 gl_){
        if (!OpenGLWrapper._enableCulling){
            OpenGLWrapper._enableCulling = true;
            gl_.glEnable(GL10.GL_CULL_FACE);
        }
    }

    public static void disableCulling(final GL10 gl_){
        if (OpenGLWrapper._enableCulling){
            OpenGLWrapper._enableCulling = false;
            gl_.glDisable(GL10.GL_CULL_FACE);
        }
    }

    public static void enableTextures(final GL10 gl_){
        if (!OpenGLWrapper._enableTextures){
            OpenGLWrapper._enableTextures = true;
            gl_.glEnable(GL10.GL_TEXTURE_2D);
        }
    }

    public static void disableTextures(final GL10 gl_){
        if (OpenGLWrapper._enableTextures){
            OpenGLWrapper._enableTextures = false;
            gl_.glDisable(GL10.GL_TEXTURE_2D);
        }
    }

    public static void enableLightning(final GL10 gl_){
        if (!OpenGLWrapper._enableLightning){
            OpenGLWrapper._enableLightning = true;
            gl_.glEnable(GL10.GL_LIGHTING);
        }
    }

    public static void disableLightning(final GL10 gl_){
        if (OpenGLWrapper._enableLightning){
            OpenGLWrapper._enableLightning = false;
            gl_.glDisable(GL10.GL_LIGHTING);
        }
    }

    public static void enableDither(final GL10 gl_){
        if (!OpenGLWrapper._enableDither){
            OpenGLWrapper._enableDither = true;
            gl_.glEnable(GL10.GL_DITHER);
            GLES20.glEnable(GLES20.GL_DITHER);
        }
    }

    public static void disableDither(final GL10 gl_){
        if (OpenGLWrapper._enableDither){
            OpenGLWrapper._enableDither = false;
            gl_.glDisable(GL10.GL_DITHER);
        }
    }

    public static void enableDepthTest(final GL10 gl_){
        if (!OpenGLWrapper._enableDepthTest){
            OpenGLWrapper._enableDepthTest = true;
            gl_.glEnable(GL10.GL_DEPTH_TEST);
        }
    }

    public static void disableDepthTest(final GL10 gl_){
        if (OpenGLWrapper._enableDepthTest){
            OpenGLWrapper._enableDepthTest = false;
            gl_.glDisable(GL10.GL_DEPTH_TEST);
        }
    }

    public static void enableMultisample(final GL10 gl_){
        if (!OpenGLWrapper._enableMultisample){
            OpenGLWrapper._enableMultisample = true;
            gl_.glEnable(GL10.GL_MULTISAMPLE);
        }
    }

    public static void disableMultisample(final GL10 gl_){
        if (OpenGLWrapper._enableMultisample){
            OpenGLWrapper._enableMultisample = false;
            gl_.glDisable(GL10.GL_MULTISAMPLE);
        }
    }

    public static void enableDepthMask(final GL10 gl_) {
        if (!_depthMaskEnabled) {
            gl_.glDepthMask(true);
            _depthMaskEnabled = true;
        }
    }

    public static void disableDepthMask(final GL10 gl_) {
        if (_depthMaskEnabled) {
            gl_.glDepthMask(false);
            _depthMaskEnabled = false;
        }
    }

    public static void bindBuffer(final GL11 gl11_, final int hardwareBufferID_){
        if (OpenGLWrapper._currentHardwareBufferID != hardwareBufferID_){
            OpenGLWrapper._currentHardwareBufferID = hardwareBufferID_;
            gl11_.glBindBuffer(GL11.GL_ARRAY_BUFFER, hardwareBufferID_);
        }
    }

    public static void deleteBuffer(final GL11 gl11_, final int hardwareBufferID_){
        OpenGLWrapper.HARDWAREBUFFERID_CONTAINER[0] = hardwareBufferID_;
        gl11_.glDeleteBuffers(1, OpenGLWrapper.HARDWAREBUFFERID_CONTAINER, 0);
    }

    public static void bindTexture(final GL10 gl_, final int hardwareTextureID_){
        if (OpenGLWrapper._currentHardwareTextureID != hardwareTextureID_){
            OpenGLWrapper._currentHardwareTextureID = hardwareTextureID_;
            gl_.glBindTexture(GL10.GL_TEXTURE_2D, hardwareTextureID_);
        }
    }

    public static void forceBindTexture(final GL10 gl_, final int hardwareTextureID_){
        OpenGLWrapper._currentHardwareTextureID = hardwareTextureID_;
        gl_.glBindTexture(GL10.GL_TEXTURE_2D, hardwareTextureID_);
    }

    public static void deleteTexture(final GL10 gl_, final int hardwareTextureID_){
        OpenGLWrapper.HARDWARETEXTUREID_CONTAINER[0] = hardwareTextureID_;
        gl_.glDeleteTextures(1, OpenGLWrapper.HARDWARETEXTUREID_CONTAINER, 0);
    }

    public static void texCoordPointer(final GL10 gl_, final FastFloatBuffer textureFloatBuffer_){
        if (OpenGLWrapper._currentTextureFloatBuffer != textureFloatBuffer_){
            OpenGLWrapper._currentTextureFloatBuffer = textureFloatBuffer_;
            gl_.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureFloatBuffer_.Buffer);
        }
    }

    public static void texCoordZeroPointer(final GL11 gl11_){
        gl11_.glTexCoordPointer(2, GL10.GL_FLOAT, 0, 0);
    }

    public static void vertexPointer(final GL10 gl_, final FastFloatBuffer vertexFloatBuffer_){
        if (OpenGLWrapper._currentVertexFloatBuffer != vertexFloatBuffer_){
            OpenGLWrapper._currentVertexFloatBuffer = vertexFloatBuffer_;
            gl_.glVertexPointer(2, GL10.GL_FLOAT, 0, vertexFloatBuffer_.Buffer);
        }
    }

    public static void vertexZeroPointer(final GL11 gl11_){
        gl11_.glVertexPointer(2, GL10.GL_FLOAT, 0, 0);
    }

    public static void blendFunction(final GL10 gl_, final int sourceBlendMode_, final int destinationBlendMode_){
        if (OpenGLWrapper._currentSourceBlendMode != sourceBlendMode_ ||
                OpenGLWrapper._currentDestinationBlendMode != destinationBlendMode_){
            OpenGLWrapper._currentSourceBlendMode = sourceBlendMode_;
            OpenGLWrapper._currentDestinationBlendMode = destinationBlendMode_;
            gl_.glBlendFunc(sourceBlendMode_, destinationBlendMode_);
        }
    }

    public static void lineWidth(final GL10 gl_, final float lineWidth_){
        if (OpenGLWrapper._lineWidth != lineWidth_){
            OpenGLWrapper._lineWidth = lineWidth_;
            gl_.glLineWidth(lineWidth_);
        }
    }

    public static void switchToModelViewMatrix(final GL10 gl_){
        if (OpenGLWrapper._currentMatrix != GL10.GL_MODELVIEW){
            OpenGLWrapper._currentMatrix = GL10.GL_MODELVIEW;
            gl_.glMatrixMode(GL10.GL_MODELVIEW);
        }
    }

    public static void switchToProjectionMatrix(final GL10 gl_){
        if (OpenGLWrapper._currentMatrix != GL10.GL_PROJECTION){
            OpenGLWrapper._currentMatrix = GL10.GL_PROJECTION;
            gl_.glMatrixMode(GL10.GL_PROJECTION);
        }
    }

    public static void setProjectionIdentityMatrix(final GL10 gl_){
        OpenGLWrapper.switchToProjectionMatrix(gl_);
        gl_.glLoadIdentity();
    }

    public static void setModelViewIdentityMatrix(final GL10 gl_){
        OpenGLWrapper.switchToModelViewMatrix(gl_);
        gl_.glLoadIdentity();
    }

    public static void setShadeModelFlat(final GL10 gl_){
        gl_.glShadeModel(GL10.GL_FLAT);
    }

    public static void setPerspectiveCorrectionHintFastest(final GL10 gl_){
        gl_.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
    }

    public static void bufferData(final GL11 gl11_, final ByteBuffer byteBuffer_, final int usage_){
        gl11_.glBufferData(GL11.GL_ARRAY_BUFFER, byteBuffer_.capacity(), byteBuffer_, usage_);
    }

    public static void glTexImage2D(final GL10 gl_, final int target_, final int level_, final Bitmap bitmap_, final int border_, final PixelFormat pixelFormat_){
        final Buffer pixelBuffer = OpenGLWrapper.getPixels(bitmap_, pixelFormat_);

        gl_.glTexImage2D(target_, level_, pixelFormat_.getGLFormat(), bitmap_.getWidth(), bitmap_.getHeight(), border_, pixelFormat_.getGLFormat(), pixelFormat_.getGLType(), pixelBuffer);
    }

    public static void glTexSubImage2D(final GL10 gl_, final int target_, final int level_, final int pXOffset, final int pYOffset, final Bitmap bitmap_, final PixelFormat pixelFormat_){
        final Buffer pixelBuffer = OpenGLWrapper.getPixels(bitmap_, pixelFormat_);

        gl_.glTexSubImage2D(target_, level_, pXOffset, pYOffset, bitmap_.getWidth(), bitmap_.getHeight(), pixelFormat_.getGLFormat(), pixelFormat_.getGLType(), pixelBuffer);
    }

    private static Buffer getPixels(final Bitmap bitmap_, final PixelFormat pixelFormat_){
        final int[] pixelsARGB_8888 = OpenGLWrapper.getPixelsARGB_8888(bitmap_);

        switch (pixelFormat_){
            case RGB_565:
                return ByteBuffer.wrap(OpenGLWrapper.convertARGB_8888toRGB_565(pixelsARGB_8888));
            case RGBA_8888:
                return IntBuffer.wrap(OpenGLWrapper.convertARGB_8888toRGBA_8888(pixelsARGB_8888));
            case RGBA_4444:
                return ByteBuffer.wrap(OpenGLWrapper.convertARGB_8888toARGB_4444(pixelsARGB_8888));
            case A_8:
                return ByteBuffer.wrap(OpenGLWrapper.convertARGB_8888toA_8(pixelsARGB_8888));
            default:
                throw new IllegalArgumentException(
                        "Unexpected " + PixelFormat.class.getSimpleName() + ": '" + pixelFormat_ + "'.");
        }
    }

    private static int[] convertARGB_8888toRGBA_8888(final int[] pixelsARGB8888_){
        if (OpenGLWrapper.IS_LITTLE_ENDIAN){
            for (int i = pixelsARGB8888_.length - 1; i >= 0; i--){
                final int pixel = pixelsARGB8888_[i];
                pixelsARGB8888_[i] = pixel & 0xFF00FF00 | (pixel & 0x000000FF) << 16 | (pixel & 0x00FF0000) >> 16;
            }
        } else{
            for (int i = pixelsARGB8888_.length - 1; i >= 0; i--){
                final int pixel = pixelsARGB8888_[i];
                pixelsARGB8888_[i] = (pixel & 0x00FFFFFF) << 8 | (pixel & 0xFF000000) >> 24;
            }
        }
        return pixelsARGB8888_;
    }

    private static byte[] convertARGB_8888toRGB_565(final int[] pixelsARGB8888_){
        final byte[] pixelsRGB_565 = new byte[pixelsARGB8888_.length * 2];
        if (OpenGLWrapper.IS_LITTLE_ENDIAN){
            for (int i = pixelsARGB8888_.length - 1, j = pixelsRGB_565.length - 1; i >= 0; i--){
                final int pixel = pixelsARGB8888_[i];

                final int red = ((pixel >> 16) & 0xFF);
                final int green = ((pixel >> 8) & 0xFF);
                final int blue = ((pixel) & 0xFF);

                pixelsRGB_565[j--] = (byte) ((red & 0xF8) | (green >> 5));
                pixelsRGB_565[j--] = (byte) (((green << 3) & 0xE0) | (blue >> 3));
            }
        } else{
            for (int i = pixelsARGB8888_.length - 1, j = pixelsRGB_565.length - 1; i >= 0; i--){
                final int pixel = pixelsARGB8888_[i];

                final int red = ((pixel >> 16) & 0xFF);
                final int green = ((pixel >> 8) & 0xFF);
                final int blue = ((pixel) & 0xFF);

                pixelsRGB_565[j--] = (byte) (((green << 3) & 0xE0) | (blue >> 3));
                pixelsRGB_565[j--] = (byte) ((red & 0xF8) | (green >> 5));
            }
        }
        return pixelsRGB_565;
    }

    private static byte[] convertARGB_8888toARGB_4444(final int[] pixelsARGB8888_){
        final byte[] pixelsARGB_4444 = new byte[pixelsARGB8888_.length * 2];
        if (OpenGLWrapper.IS_LITTLE_ENDIAN){
            for (int i = pixelsARGB8888_.length - 1, j = pixelsARGB_4444.length - 1; i >= 0; i--){
                final int pixel = pixelsARGB8888_[i];

                final int alpha = ((pixel >> 28) & 0x0F);
                final int red = ((pixel >> 16) & 0xF0);
                final int green = ((pixel >> 8) & 0xF0);
                final int blue = ((pixel) & 0x0F);

                pixelsARGB_4444[j--] = (byte) (alpha | red);
                pixelsARGB_4444[j--] = (byte) (green | blue);
            }
        } else{
            for (int i = pixelsARGB8888_.length - 1, j = pixelsARGB_4444.length - 1; i >= 0; i--){
                final int pixel = pixelsARGB8888_[i];

                final int alpha = ((pixel >> 28) & 0x0F);
                final int red = ((pixel >> 16) & 0xF0);
                final int green = ((pixel >> 8) & 0xF0);
                final int blue = ((pixel) & 0x0F);

                pixelsARGB_4444[j--] = (byte) (green | blue);
                pixelsARGB_4444[j--] = (byte) (alpha | red);
            }
        }
        return pixelsARGB_4444;
    }

    private static byte[] convertARGB_8888toA_8(final int[] pixelsARGB8888_){
        final byte[] pixelsA_8 = new byte[pixelsARGB8888_.length];
        if (OpenGLWrapper.IS_LITTLE_ENDIAN){
            for (int i = pixelsARGB8888_.length - 1; i >= 0; i--){
                pixelsA_8[i] = (byte) (pixelsARGB8888_[i] >> 24);
            }
        } else{
            for (int i = pixelsARGB8888_.length - 1; i >= 0; i--){
                pixelsA_8[i] = (byte) (pixelsARGB8888_[i] & 0xFF);
            }
        }
        return pixelsA_8;
    }

    public static int[] getPixelsARGB_8888(final Bitmap bitmap_){
        final int w = bitmap_.getWidth();
        final int h = bitmap_.getHeight();

        final int[] pixelsARGB_8888 = new int[w * h];
        bitmap_.getPixels(pixelsARGB_8888, 0, w, 0, 0, w, h);

        return pixelsARGB_8888;
    }

    public static void checkGLError(final GL10 gl_) throws GLException{
        final int err = gl_.glGetError();
        if (err != GL10.GL_NO_ERROR){
            throw new GLException(err);
        }
    }
}
